package helpers

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"testing"
	"time"

	"v8s-controller/internal/configuration"
	"v8s-controller/internal/db"
	"v8s/logger"

	"github.com/labstack/gommon/log"
	"github.com/stretchr/testify/assert"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.uber.org/zap"
)

type MongoHelper struct {
	log *zap.SugaredLogger

	client     *db.MongoClient
	properties configuration.MongoProperties
}

func CreateMongoHelper(client *db.MongoClient, properties configuration.MongoProperties) *MongoHelper {
	return &MongoHelper{
		log: logger.Logger().Named("mongo-helper").Sugar(),

		client:     client,
		properties: properties,
	}
}

func (h *MongoHelper) InsertProjects(file string) {
	data, err := ioutil.ReadFile(file)
	if err != nil {
		panic(fmt.Sprintf("cannot open file '%s': %s", file, err))
	}

	var projects []map[string]interface{}
	err = json.Unmarshal(data, &projects)
	if err != nil {
		panic(fmt.Sprintf("failed to unmarshal file '%s': %s", file, err))
	}

	documents := make([]interface{}, 0, len(projects))
	for _, project := range projects {
		current := project
		iterate(current, func(parent map[string]interface{}, key string) {
			switch typed := parent[key].(type) {
			case time.Time:
				parent[key] = primitive.NewDateTimeFromTime(typed)
			case string:
				parsed, err := time.Parse(time.RFC3339, typed)
				if err != nil {
					return
				}

				parent[key] = primitive.NewDateTimeFromTime(parsed)
			default:
				return
			}
		})

		documents = append(documents, current)
	}

	inserted, err := h.client.Database(h.properties.Database).Collection(db.ProjectsCollection).InsertMany(context.Background(), documents)
	if err != nil {
		panic(fmt.Sprintf("failed to insert documents from '%s': %s", file, err))
	}

	log.Infof("inserted: '%s'", inserted.InsertedIDs)
}

func (h *MongoHelper) Clear(ctx context.Context) {
	err := h.client.Database(h.properties.Database).Drop(ctx)
	if err != nil {
		panic(fmt.Errorf("failed to drop database: %s", err))
	}
}

func (h *MongoHelper) AssertProjects(t *testing.T, file string) {
	expected, err := ioutil.ReadFile(file)
	if err != nil {
		panic(fmt.Errorf("failed to open file '%s': %w", file, err))
	}

	cursor, err := h.client.Database(h.properties.Database).Collection(db.ProjectsCollection).Find(context.Background(), bson.M{})
	if err != nil {
		panic(fmt.Errorf("failed to list documents from mongodb: %w", err))
	}

	documents := make([]map[string]interface{}, 0)
	if err = cursor.All(context.Background(), &documents); err != nil {
		panic(fmt.Errorf("failed to read from cursor: %w", err))
	}

	for _, document := range documents {
		delete(document, "_id")

		iterate(document, func(parent map[string]interface{}, key string) {
			if parent[key] == nil {
				delete(parent, key)
				return
			}

			switch typed := parent[key].(type) {
			case time.Time:
				mock := mockedTime(typed.UTC())
				parent[key] = mock
			case primitive.DateTime:
				mock := mockedTime(typed.Time().UTC())
				parent[key] = mock
			case string:
				time, err := time.Parse(time.RFC3339, typed)
				if err != nil {
					break
				}

				mock := mockedTime(time.UTC())
				parent[key] = mock
			}
		})
	}

	actual, err := json.MarshalIndent(documents, "", "  ")
	if err != nil {
		panic(fmt.Errorf("failed to marshal documents: %w", err))
	}

	assert.Equal(t, string(expected), string(actual))
}

func mockedTime(utc time.Time) time.Time {
	if utc.Before(time.Now().Add(time.Minute)) {
		utc = time.Date(2000, 1, 14, 0, 0, 0, 0, time.UTC)
	}

	return utc.
		Add(-time.Hour * time.Duration(utc.Hour())).
		Add(-time.Minute * time.Duration(utc.Minute())).
		Add(-time.Second * time.Duration(utc.Second())).
		Add(-time.Nanosecond * time.Duration(utc.Nanosecond()))
}
